aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--backend/auth/auth.go (renamed from backend/auth.go)6
-rw-r--r--backend/auth/jwt.go54
-rw-r--r--backend/db/query.sql.go31
-rw-r--r--backend/go.mod6
-rw-r--r--backend/go.sum15
-rw-r--r--backend/main.go17
-rw-r--r--backend/query.sql10
-rw-r--r--frontend/app/routes/dashboard.tsx22
-rw-r--r--frontend/app/routes/login.tsx31
-rw-r--r--frontend/app/services/auth.server.ts141
-rw-r--r--frontend/app/services/session.server.ts13
-rw-r--r--frontend/package-lock.json40
-rw-r--r--frontend/package.json3
13 files changed, 368 insertions, 21 deletions
diff --git a/backend/auth.go b/backend/auth/auth.go
index 7cf0c58..40e54a6 100644
--- a/backend/auth.go
+++ b/backend/auth/auth.go
@@ -1,4 +1,4 @@
-package main
+package auth
import (
"context"
@@ -7,8 +7,8 @@ import (
"github.com/nsfisis/iosdc-2024-albatross-backend/db"
)
-func authLogin(ctx context.Context, queries *db.Queries, username, password string) (int, error) {
- userAuth, err := queries.GetUserAuthFromUsername(ctx, username)
+func Login(ctx context.Context, queries *db.Queries, username, password string) (int, error) {
+ userAuth, err := queries.GetUserAuthByUsername(ctx, username)
if err != nil {
return 0, err
}
diff --git a/backend/auth/jwt.go b/backend/auth/jwt.go
new file mode 100644
index 0000000..aa35de6
--- /dev/null
+++ b/backend/auth/jwt.go
@@ -0,0 +1,54 @@
+package auth
+
+import (
+ "time"
+
+ "github.com/golang-jwt/jwt/v5"
+ echojwt "github.com/labstack/echo-jwt/v4"
+ "github.com/labstack/echo/v4"
+
+ "github.com/nsfisis/iosdc-2024-albatross-backend/db"
+)
+
+type JWTClaims struct {
+ UserID int `json:"user_id"`
+ Username string `json:"username"`
+ DisplayUsername string `json:"display_username"`
+ IconPath *string `json:"icon_path"`
+ IsAdmin bool `json:"is_admin"`
+ jwt.RegisteredClaims
+}
+
+func NewJWT(user *db.User) (string, error) {
+ var iconPath *string
+ if user.IconPath.Valid {
+ iconPath = &user.IconPath.String
+ }
+ claims := &JWTClaims{
+ UserID: int(user.UserID),
+ Username: user.Username,
+ DisplayUsername: user.DisplayUsername,
+ IconPath: iconPath,
+ IsAdmin: user.IsAdmin,
+ RegisteredClaims: jwt.RegisteredClaims{
+ ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)),
+ },
+ }
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
+ return token.SignedString([]byte("TODO"))
+}
+
+func NewJWTMiddleware() echo.MiddlewareFunc {
+ return echojwt.WithConfig(echojwt.Config{
+ NewClaimsFunc: func(c echo.Context) jwt.Claims {
+ return new(JWTClaims)
+ },
+ SigningKey: []byte("TODO"),
+ })
+}
+
+func GetJWTClaimsFromEchoContext(c echo.Context) *JWTClaims {
+ user := c.Get("user").(*jwt.Token)
+ claims := user.Claims.(*JWTClaims)
+ return claims
+}
diff --git a/backend/db/query.sql.go b/backend/db/query.sql.go
index 5c4f910..72c6502 100644
--- a/backend/db/query.sql.go
+++ b/backend/db/query.sql.go
@@ -11,13 +11,14 @@ import (
"github.com/jackc/pgx/v5/pgtype"
)
-const getUserAuthFromUsername = `-- name: GetUserAuthFromUsername :one
+const getUserAuthByUsername = `-- name: GetUserAuthByUsername :one
SELECT users.user_id, username, display_username, icon_path, is_admin, created_at, user_auth_id, user_auths.user_id, auth_type, password_hash FROM users
JOIN user_auths ON users.user_id = user_auths.user_id
WHERE users.username = $1
+LIMIT 1
`
-type GetUserAuthFromUsernameRow struct {
+type GetUserAuthByUsernameRow struct {
UserID int32
Username string
DisplayUsername string
@@ -30,9 +31,9 @@ type GetUserAuthFromUsernameRow struct {
PasswordHash pgtype.Text
}
-func (q *Queries) GetUserAuthFromUsername(ctx context.Context, username string) (GetUserAuthFromUsernameRow, error) {
- row := q.db.QueryRow(ctx, getUserAuthFromUsername, username)
- var i GetUserAuthFromUsernameRow
+func (q *Queries) GetUserAuthByUsername(ctx context.Context, username string) (GetUserAuthByUsernameRow, error) {
+ row := q.db.QueryRow(ctx, getUserAuthByUsername, username)
+ var i GetUserAuthByUsernameRow
err := row.Scan(
&i.UserID,
&i.Username,
@@ -47,3 +48,23 @@ func (q *Queries) GetUserAuthFromUsername(ctx context.Context, username string)
)
return i, err
}
+
+const getUserById = `-- name: GetUserById :one
+SELECT user_id, username, display_username, icon_path, is_admin, created_at FROM users
+WHERE users.user_id = $1
+LIMIT 1
+`
+
+func (q *Queries) GetUserById(ctx context.Context, userID int32) (User, error) {
+ row := q.db.QueryRow(ctx, getUserById, userID)
+ var i User
+ err := row.Scan(
+ &i.UserID,
+ &i.Username,
+ &i.DisplayUsername,
+ &i.IconPath,
+ &i.IsAdmin,
+ &i.CreatedAt,
+ )
+ return i, err
+}
diff --git a/backend/go.mod b/backend/go.mod
index 38020c7..4203c35 100644
--- a/backend/go.mod
+++ b/backend/go.mod
@@ -3,8 +3,11 @@ module github.com/nsfisis/iosdc-2024-albatross-backend
go 1.22.3
require (
+ github.com/golang-jwt/jwt/v5 v5.2.1
github.com/gorilla/websocket v1.5.3
github.com/jackc/pgx/v5 v5.5.5
+ github.com/labstack/echo-jwt/v4 v4.2.0
+ github.com/labstack/echo/v4 v4.12.0
github.com/sqlc-dev/sqlc v1.26.0
)
@@ -17,6 +20,7 @@ require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/structtag v1.2.0 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
+ github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/cel-go v0.20.1 // indirect
github.com/google/uuid v1.6.0 // indirect
@@ -26,7 +30,6 @@ require (
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
- github.com/labstack/echo/v4 v4.12.0 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
@@ -54,6 +57,7 @@ require (
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
+ golang.org/x/time v0.5.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect
google.golang.org/grpc v1.62.1 // indirect
diff --git a/backend/go.sum b/backend/go.sum
index 29c1143..38a504b 100644
--- a/backend/go.sum
+++ b/backend/go.sum
@@ -18,6 +18,10 @@ github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
+github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
+github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
+github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
+github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
@@ -53,13 +57,14 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/labstack/echo-jwt/v4 v4.2.0 h1:odSISV9JgcSCuhgQSV/6Io3i7nUmfM/QkBeR5GVJj5c=
+github.com/labstack/echo-jwt/v4 v4.2.0/go.mod h1:MA2RqdXdEn4/uEglx0HcUOgQSyBaTh5JcaHIan3biwU=
github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
-github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
@@ -130,8 +135,6 @@ go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg=
-golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 h1:mchzmB1XO2pMaKFRqk/+MV3mgGG96aqaPXaMifQU47w=
@@ -141,8 +144,6 @@ golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
-golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -151,13 +152,13 @@ golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
-golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
+golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
diff --git a/backend/main.go b/backend/main.go
index 26859e5..e878bed 100644
--- a/backend/main.go
+++ b/backend/main.go
@@ -11,6 +11,7 @@ import (
"github.com/jackc/pgx/v5"
"github.com/labstack/echo/v4"
+ "github.com/nsfisis/iosdc-2024-albatross-backend/auth"
"github.com/nsfisis/iosdc-2024-albatross-backend/db"
)
@@ -123,7 +124,7 @@ func handleApiLogin(c echo.Context, queries *db.Queries) error {
}
type LoginResponseData struct {
- UserId int `json:"userId"`
+ Token string `json:"token"`
}
ctx := c.Request().Context()
@@ -133,13 +134,23 @@ func handleApiLogin(c echo.Context, queries *db.Queries) error {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
- userId, err := authLogin(ctx, queries, requestData.Username, requestData.Password)
+ userId, err := auth.Login(ctx, queries, requestData.Username, requestData.Password)
if err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, err.Error())
}
+ user, err := queries.GetUserById(ctx, int32(userId))
+ if err != nil {
+ return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
+ }
+
+ jwt, err := auth.NewJWT(&user)
+ if err != nil {
+ return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
+ }
+
responseData := LoginResponseData{
- UserId: userId,
+ Token: jwt,
}
return c.JSON(http.StatusOK, responseData)
diff --git a/backend/query.sql b/backend/query.sql
index bdcd657..165c2c9 100644
--- a/backend/query.sql
+++ b/backend/query.sql
@@ -1,4 +1,10 @@
--- name: GetUserAuthFromUsername :one
+-- name: GetUserById :one
+SELECT * FROM users
+WHERE users.user_id = $1
+LIMIT 1;
+
+-- name: GetUserAuthByUsername :one
SELECT * FROM users
JOIN user_auths ON users.user_id = user_auths.user_id
-WHERE users.username = $1;
+WHERE users.username = $1
+LIMIT 1;
diff --git a/frontend/app/routes/dashboard.tsx b/frontend/app/routes/dashboard.tsx
new file mode 100644
index 0000000..be274eb
--- /dev/null
+++ b/frontend/app/routes/dashboard.tsx
@@ -0,0 +1,22 @@
+import type { LoaderFunctionArgs } from "@remix-run/node";
+import { isAuthenticated } from "../services/auth.server";
+import { useLoaderData } from "@remix-run/react";
+
+export async function loader({ request }: LoaderFunctionArgs) {
+ return await isAuthenticated(request, {
+ failureRedirect: "/login",
+ });
+}
+
+export default function Dashboard() {
+ const user = useLoaderData<typeof loader>()!;
+
+ return (
+ <div>
+ <h1>
+ #{user.userId} {user.displayUsername} (@{user.username})
+ </h1>
+ {user.isAdmin && <p>Admin</p>}
+ </div>
+ );
+}
diff --git a/frontend/app/routes/login.tsx b/frontend/app/routes/login.tsx
new file mode 100644
index 0000000..cf5be14
--- /dev/null
+++ b/frontend/app/routes/login.tsx
@@ -0,0 +1,31 @@
+import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
+import { Form } from "@remix-run/react";
+import { authenticator } from "../services/auth.server";
+
+export async function loader({ request }: LoaderFunctionArgs) {
+ return await authenticator.isAuthenticated(request, {
+ successRedirect: "/dashboard",
+ });
+}
+
+export async function action({ request }: ActionFunctionArgs) {
+ return await authenticator.authenticate("default", request, {
+ successRedirect: "/dashboard",
+ failureRedirect: "/login",
+ });
+}
+
+export default function Login() {
+ return (
+ <Form method="post">
+ <input type="username" name="username" required />
+ <input
+ type="password"
+ name="password"
+ autoComplete="current-password"
+ required
+ />
+ <button>Log In</button>
+ </Form>
+ );
+}
diff --git a/frontend/app/services/auth.server.ts b/frontend/app/services/auth.server.ts
new file mode 100644
index 0000000..144a7cd
--- /dev/null
+++ b/frontend/app/services/auth.server.ts
@@ -0,0 +1,141 @@
+import { Authenticator } from "remix-auth";
+import { FormStrategy } from "remix-auth-form";
+import { sessionStorage } from "./session.server";
+import { jwtDecode } from "jwt-decode";
+import type { Session } from "@remix-run/server-runtime";
+
+export const authenticator = new Authenticator<string>(sessionStorage);
+
+async function login(username: string, password: string): Promise<string> {
+ const res = await fetch(`http://api-server/api/login`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ username, password }),
+ });
+ if (!res.ok) {
+ throw new Error("Invalid username or password");
+ }
+ const user = await res.json();
+ return user.token;
+}
+
+authenticator.use(
+ new FormStrategy(async ({ form }) => {
+ const username = String(form.get("username"));
+ const password = String(form.get("password"));
+ return await login(username, password);
+ }),
+ "default",
+);
+
+type JwtPayload = {
+ user_id: number;
+ username: string;
+ display_username: string;
+ icon_path: string | null;
+ is_admin: boolean;
+};
+
+export type User = {
+ userId: number;
+ username: string;
+ displayUsername: string;
+ iconPath: string | null;
+ isAdmin: boolean;
+};
+
+export async function isAuthenticated(
+ request: Request | Session,
+ options?: {
+ successRedirect?: never;
+ failureRedirect?: never;
+ headers?: never;
+ },
+): Promise<User | null>;
+export async function isAuthenticated(
+ request: Request | Session,
+ options: {
+ successRedirect: string;
+ failureRedirect?: never;
+ headers?: HeadersInit;
+ },
+): Promise<null>;
+export async function isAuthenticated(
+ request: Request | Session,
+ options: {
+ successRedirect?: never;
+ failureRedirect: string;
+ headers?: HeadersInit;
+ },
+): Promise<User>;
+export async function isAuthenticated(
+ request: Request | Session,
+ options: {
+ successRedirect: string;
+ failureRedirect: string;
+ headers?: HeadersInit;
+ },
+): Promise<null>;
+export async function isAuthenticated(
+ request: Request | Session,
+ options:
+ | {
+ successRedirect?: never;
+ failureRedirect?: never;
+ headers?: never;
+ }
+ | {
+ successRedirect: string;
+ failureRedirect?: never;
+ headers?: HeadersInit;
+ }
+ | {
+ successRedirect?: never;
+ failureRedirect: string;
+ headers?: HeadersInit;
+ }
+ | {
+ successRedirect: string;
+ failureRedirect: string;
+ headers?: HeadersInit;
+ } = {},
+): Promise<User | null> {
+ let jwt;
+
+ // This function's signature should be compatible with `authenticator.isAuthenticated` but TypeScript does not infer it correctly.
+ const { successRedirect, failureRedirect, headers } = options;
+ if (successRedirect && failureRedirect) {
+ jwt = await authenticator.isAuthenticated(request, {
+ successRedirect,
+ failureRedirect,
+ headers,
+ });
+ } else if (!successRedirect && failureRedirect) {
+ jwt = await authenticator.isAuthenticated(request, {
+ failureRedirect,
+ headers,
+ });
+ } else if (successRedirect && !failureRedirect) {
+ jwt = await authenticator.isAuthenticated(request, {
+ successRedirect,
+ headers,
+ });
+ } else {
+ jwt = await authenticator.isAuthenticated(request);
+ }
+
+ if (!jwt) {
+ return null;
+ }
+ // TODO: runtime type check
+ const payload = jwtDecode<JwtPayload>(jwt);
+ return {
+ userId: payload.user_id,
+ username: payload.username,
+ displayUsername: payload.display_username,
+ iconPath: payload.icon_path,
+ isAdmin: payload.is_admin,
+ };
+}
diff --git a/frontend/app/services/session.server.ts b/frontend/app/services/session.server.ts
new file mode 100644
index 0000000..2000853
--- /dev/null
+++ b/frontend/app/services/session.server.ts
@@ -0,0 +1,13 @@
+import { createCookieSessionStorage } from "@remix-run/node";
+
+export const sessionStorage = createCookieSessionStorage({
+ cookie: {
+ name: "albatross_session",
+ sameSite: "lax",
+ path: "/",
+ httpOnly: true,
+ secrets: ["TODO"],
+ // secure: process.env.NODE_ENV === "production",
+ secure: false, // TODO
+ },
+});
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 615214d..2186d40 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -10,9 +10,12 @@
"@remix-run/react": "^2.10.3",
"@remix-run/serve": "^2.10.3",
"isbot": "^5.1.13",
+ "jwt-decode": "^4.0.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-use-websocket": "^4.8.1",
+ "remix-auth": "^3.7.0",
+ "remix-auth-form": "^1.5.0",
"use-debounce": "^10.0.1"
},
"devDependencies": {
@@ -6268,6 +6271,14 @@
"node": ">=4.0"
}
},
+ "node_modules/jwt-decode": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
+ "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -8931,6 +8942,27 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/remix-auth": {
+ "version": "3.7.0",
+ "resolved": "https://registry.npmjs.org/remix-auth/-/remix-auth-3.7.0.tgz",
+ "integrity": "sha512-2QVjp2nJVaYxuFBecMQwzixCO7CLSssttLBU5eVlNcNlVeNMmY1g7OkmZ1Ogw9sBcoMXZ18J7xXSK0AISVFcfQ==",
+ "dependencies": {
+ "uuid": "^8.3.2"
+ },
+ "peerDependencies": {
+ "@remix-run/react": "^1.0.0 || ^2.0.0",
+ "@remix-run/server-runtime": "^1.0.0 || ^2.0.0"
+ }
+ },
+ "node_modules/remix-auth-form": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/remix-auth-form/-/remix-auth-form-1.5.0.tgz",
+ "integrity": "sha512-xWM7T41vi4ZsIxL3f8gz/D6g2mxrnYF7LnG+rG3VqwHh6l13xCoKLraxzWRdbKMVKKQCMISKZRXAeJh9/PQwBA==",
+ "peerDependencies": {
+ "@remix-run/server-runtime": "^1.0.0 || ^2.0.0",
+ "remix-auth": "^3.6.0"
+ }
+ },
"node_modules/require-like": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz",
@@ -10552,6 +10584,14 @@
"node": ">= 0.4.0"
}
},
+ "node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"node_modules/uvu": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index e215f0c..c3ac2f5 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -15,9 +15,12 @@
"@remix-run/react": "^2.10.3",
"@remix-run/serve": "^2.10.3",
"isbot": "^5.1.13",
+ "jwt-decode": "^4.0.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-use-websocket": "^4.8.1",
+ "remix-auth": "^3.7.0",
+ "remix-auth-form": "^1.5.0",
"use-debounce": "^10.0.1"
},
"devDependencies": {