User creation
This commit is contained in:
parent
a28f9fd38c
commit
e0fd7097eb
|
@ -26,6 +26,13 @@ const services = struct {
|
|||
const invites = @import("./api/invites.zig");
|
||||
};
|
||||
|
||||
pub const RegistrationRequest = struct {
|
||||
username: []const u8,
|
||||
password: []const u8,
|
||||
invite_code: []const u8,
|
||||
email: ?[]const u8,
|
||||
};
|
||||
|
||||
pub const InviteRequest = struct {
|
||||
pub const Type = services.invites.InviteType;
|
||||
|
||||
|
@ -69,13 +76,6 @@ pub fn firstIndexOf(str: []const u8, ch: u8) ?usize {
|
|||
|
||||
pub const Scheme = models.Community.Scheme;
|
||||
|
||||
pub const RegistrationInfo = struct {
|
||||
username: []const u8,
|
||||
password: []const u8,
|
||||
email: ?[]const u8,
|
||||
invite_code: ?[]const u8,
|
||||
};
|
||||
|
||||
pub const LoginResult = struct {
|
||||
user_id: Uuid,
|
||||
token: [token_str_len]u8,
|
||||
|
@ -257,5 +257,31 @@ fn ApiConn(comptime DbConn: type) type {
|
|||
.invite_type = options.invite_type,
|
||||
}, self.arena.allocator());
|
||||
}
|
||||
|
||||
pub fn register(self: *Self, request: RegistrationRequest) !services.users.User {
|
||||
std.log.debug("registering user {s} with code {s}", .{ request.username, request.invite_code });
|
||||
const invite = try services.invites.getByCode(&self.db, request.invite_code, self.arena.allocator());
|
||||
|
||||
if (!Uuid.eql(invite.to_community, self.community_id)) return error.NotFound;
|
||||
if (invite.max_uses != null and invite.times_used >= invite.max_uses.?) return error.InviteExpired;
|
||||
if (invite.expires_at != null and DateTime.now().isAfter(invite.expires_at.?)) return error.InviteExpired;
|
||||
|
||||
if (self.community_id == null) @panic("Unimplmented");
|
||||
|
||||
const user_id = try services.users.create(&self.db, request.username, request.password, self.community_id, .{ .invite_id = invite.id, .email = request.email }, self.internal_alloc);
|
||||
|
||||
switch (invite.invite_type) {
|
||||
.user => {},
|
||||
.system => @panic("System user invites unimplemented"),
|
||||
.community_owner => {
|
||||
try services.communities.transferOwnership(&self.db, self.community_id.?, user_id);
|
||||
},
|
||||
}
|
||||
|
||||
return services.users.get(&self.db, user_id, self.arena.allocator()) catch |err| switch (err) {
|
||||
error.NotFound => error.Unexpected,
|
||||
else => err,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ pub const Scheme = enum {
|
|||
pub const Community = struct {
|
||||
id: Uuid,
|
||||
|
||||
owner_id: ?Uuid,
|
||||
host: []const u8,
|
||||
name: []const u8,
|
||||
|
||||
|
@ -61,6 +62,7 @@ pub fn create(db: anytype, origin: []const u8, name: ?[]const u8) CreateError!Co
|
|||
|
||||
const community = Community{
|
||||
.id = id,
|
||||
.owner_id = null,
|
||||
.host = host,
|
||||
.name = name orelse host,
|
||||
.scheme = scheme,
|
||||
|
@ -84,12 +86,17 @@ fn firstIndexOf(str: []const u8, ch: u8) ?usize {
|
|||
}
|
||||
|
||||
pub fn getByHost(db: anytype, host: []const u8, alloc: std.mem.Allocator) !Community {
|
||||
const result = (try db.execRow(&.{ Uuid, []const u8, []const u8, Scheme }, "SELECT id, host, name, scheme FROM community WHERE host = ?", .{host}, alloc)) orelse return error.NotFound;
|
||||
const result = (try db.execRow(&.{ Uuid, ?Uuid, []const u8, []const u8, Scheme }, "SELECT id, owner_id, host, name, scheme FROM community WHERE host = ?", .{host}, alloc)) orelse return error.NotFound;
|
||||
|
||||
return Community{
|
||||
.id = result[0],
|
||||
.host = result[1],
|
||||
.name = result[2],
|
||||
.scheme = result[3],
|
||||
.owner_id = result[1],
|
||||
.host = result[2],
|
||||
.name = result[3],
|
||||
.scheme = result[4],
|
||||
};
|
||||
}
|
||||
|
||||
pub fn transferOwnership(db: anytype, community_id: Uuid, new_owner: Uuid) !void {
|
||||
_ = try db.execRow(&.{i64}, "UPDATE community SET owner_id = ? WHERE id = ?", .{ new_owner, community_id }, null);
|
||||
}
|
||||
|
|
|
@ -48,6 +48,21 @@ pub const Invite = struct {
|
|||
invite_type: InviteType,
|
||||
};
|
||||
|
||||
const DbModel = struct {
|
||||
id: Uuid,
|
||||
|
||||
created_by: Uuid, // User ID
|
||||
to_community: ?Uuid,
|
||||
name: []const u8,
|
||||
code: []const u8,
|
||||
|
||||
created_at: DateTime,
|
||||
expires_at: ?DateTime,
|
||||
|
||||
max_uses: ?usize,
|
||||
|
||||
@"type": InviteType,
|
||||
};
|
||||
fn cloneStr(str: []const u8, alloc: std.mem.Allocator) ![]const u8 {
|
||||
const new = try alloc.alloc(u8, str.len);
|
||||
std.mem.copy(u8, new, str);
|
||||
|
@ -74,11 +89,12 @@ pub fn create(db: anytype, created_by: Uuid, to_community: ?Uuid, options: Invit
|
|||
try cloneStr(name, alloc)
|
||||
else
|
||||
try cloneStr(code, alloc);
|
||||
errdefer alloc.free(name);
|
||||
|
||||
const id = Uuid.randV4(getRandom());
|
||||
const created_at = DateTime.now();
|
||||
|
||||
const invite = Invite{
|
||||
try db.insert("invite", DbModel{
|
||||
.id = id,
|
||||
|
||||
.created_by = created_by,
|
||||
|
@ -87,15 +103,58 @@ pub fn create(db: anytype, created_by: Uuid, to_community: ?Uuid, options: Invit
|
|||
.code = code,
|
||||
|
||||
.created_at = created_at,
|
||||
.times_used = 0,
|
||||
|
||||
.expires_at = options.expires_at,
|
||||
|
||||
.max_uses = options.max_uses,
|
||||
|
||||
.@"type" = options.invite_type,
|
||||
});
|
||||
|
||||
return Invite{
|
||||
.id = id,
|
||||
|
||||
.created_by = created_by,
|
||||
.to_community = to_community,
|
||||
.name = name,
|
||||
.code = code,
|
||||
|
||||
.created_at = created_at,
|
||||
.expires_at = options.expires_at,
|
||||
|
||||
.times_used = 0,
|
||||
.max_uses = options.max_uses,
|
||||
|
||||
.invite_type = options.invite_type,
|
||||
};
|
||||
|
||||
try db.insert("invite", invite);
|
||||
|
||||
return invite;
|
||||
}
|
||||
|
||||
pub fn getByCode(db: anytype, code: []const u8, alloc: std.mem.Allocator) !Invite {
|
||||
const code_clone = try cloneStr(code, alloc);
|
||||
const info = (try db.execRow(&.{ Uuid, Uuid, Uuid, []const u8, DateTime, ?DateTime, usize, ?usize, InviteType },
|
||||
\\SELECT
|
||||
\\ invite.id, invite.created_by, invite.to_community, invite.name,
|
||||
\\ invite.created_at, invite.expires_at,
|
||||
\\ COUNT(local_user.user_id) as uses, invite.max_uses,
|
||||
\\ invite.type
|
||||
\\FROM invite LEFT OUTER JOIN local_user ON invite.id = local_user.invite_id
|
||||
\\WHERE invite.code = ?
|
||||
\\GROUP BY invite.id
|
||||
, .{code}, alloc)) orelse return error.NotFound;
|
||||
|
||||
return Invite{
|
||||
.id = info[0],
|
||||
|
||||
.created_by = info[1],
|
||||
.to_community = info[2],
|
||||
.name = info[3],
|
||||
.code = code_clone,
|
||||
|
||||
.created_at = info[4],
|
||||
.expires_at = info[5],
|
||||
|
||||
.times_used = info[6],
|
||||
.max_uses = info[7],
|
||||
|
||||
.invite_type = info[8],
|
||||
};
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ const util = @import("util");
|
|||
const auth = @import("./auth.zig");
|
||||
|
||||
const Uuid = util.Uuid;
|
||||
const DateTime = util.DateTime;
|
||||
const getRandom = @import("../api.zig").getRandom;
|
||||
|
||||
const UserAuthInfo = struct {
|
||||
|
@ -16,14 +17,14 @@ pub const CreateError = error{
|
|||
DbError,
|
||||
};
|
||||
|
||||
const User = struct {
|
||||
const DbUser = struct {
|
||||
id: Uuid,
|
||||
|
||||
username: []const u8,
|
||||
community_id: ?Uuid,
|
||||
};
|
||||
|
||||
const LocalUser = struct {
|
||||
const DbLocalUser = struct {
|
||||
user_id: Uuid,
|
||||
|
||||
invite_id: ?Uuid,
|
||||
|
@ -72,7 +73,7 @@ pub fn create(
|
|||
password: []const u8,
|
||||
community_id: ?Uuid,
|
||||
options: CreateOptions,
|
||||
alloc: std.mem.Allocator,
|
||||
password_alloc: std.mem.Allocator,
|
||||
) CreateError!Uuid {
|
||||
const id = Uuid.randV4(getRandom());
|
||||
if ((try lookupByUsername(db, username, community_id)) != null) {
|
||||
|
@ -84,7 +85,7 @@ pub fn create(
|
|||
.username = username,
|
||||
.community_id = community_id,
|
||||
}) catch return error.DbError;
|
||||
try auth.passwords.create(db, id, password, alloc);
|
||||
try auth.passwords.create(db, id, password, password_alloc);
|
||||
db.insert("local_user", .{
|
||||
.user_id = id,
|
||||
.invite_id = options.invite_id,
|
||||
|
@ -93,3 +94,35 @@ pub fn create(
|
|||
|
||||
return id;
|
||||
}
|
||||
|
||||
pub const User = struct {
|
||||
id: Uuid,
|
||||
|
||||
username: []const u8,
|
||||
host: []const u8,
|
||||
|
||||
community_id: Uuid,
|
||||
|
||||
created_at: DateTime,
|
||||
};
|
||||
|
||||
pub fn get(db: anytype, id: Uuid, alloc: std.mem.Allocator) !User {
|
||||
const result = (try db.execRow(
|
||||
&.{ []const u8, []const u8, Uuid, DateTime },
|
||||
\\SELECT user.username, community.host, community.id, user.created_at
|
||||
\\FROM user JOIN community ON user.community_id = community.id
|
||||
\\WHERE user.id = ?
|
||||
\\LIMIT 1
|
||||
,
|
||||
.{id},
|
||||
alloc,
|
||||
)) orelse return error.NotFound;
|
||||
|
||||
return User{
|
||||
.id = id,
|
||||
.username = result[0],
|
||||
.host = result[1],
|
||||
.community_id = result[2],
|
||||
.created_at = result[3],
|
||||
};
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ const Uuid = @import("util").Uuid;
|
|||
pub const auth = @import("./controllers/auth.zig");
|
||||
pub const communities = @import("./controllers/communities.zig");
|
||||
pub const invites = @import("./controllers/invites.zig");
|
||||
pub const users = @import("./controllers/users.zig");
|
||||
|
||||
pub const utils = struct {
|
||||
const json_options = if (builtin.mode == .Debug) .{
|
||||
|
|
|
@ -7,7 +7,7 @@ const RequestServer = root.RequestServer;
|
|||
const RouteArgs = http.RouteArgs;
|
||||
|
||||
pub const create = struct {
|
||||
pub const method = .GET;
|
||||
pub const method = .POST;
|
||||
pub const path = "/communities";
|
||||
pub fn handler(srv: *RequestServer, ctx: *http.server.Context, _: RouteArgs) !void {
|
||||
const opt = try utils.parseRequestBody(struct { origin: []const u8 }, ctx);
|
||||
|
|
|
@ -4,38 +4,27 @@ const builtin = @import("builtin");
|
|||
const http = @import("http");
|
||||
const Uuid = @import("util").Uuid;
|
||||
|
||||
const RegistrationInfo = @import("../api.zig").RegistrationInfo;
|
||||
const RegistrationRequest = @import("../api.zig").RegistrationRequest;
|
||||
const utils = @import("../controllers.zig").utils;
|
||||
|
||||
const RequestServer = root.RequestServer;
|
||||
const RouteArgs = http.RouteArgs;
|
||||
|
||||
pub fn register(srv: *RequestServer, ctx: *http.server.Context, _: RouteArgs) !void {
|
||||
const info = try utils.parseRequestBody(RegistrationInfo, ctx);
|
||||
pub const create = struct {
|
||||
pub const method = .POST;
|
||||
pub const path = "/users";
|
||||
pub fn handler(srv: *RequestServer, ctx: *http.server.Context, _: RouteArgs) !void {
|
||||
const info = try utils.parseRequestBody(RegistrationRequest, ctx);
|
||||
defer utils.freeRequestBody(info, ctx.alloc);
|
||||
|
||||
var api = try utils.getApiConn(srv, ctx);
|
||||
defer api.close();
|
||||
|
||||
const user = api.register(info) catch |err| switch (err) {
|
||||
error.UsernameUnavailable => return utils.respondError(ctx, .bad_request, "Username Unavailable"),
|
||||
error.UsernameTaken => return utils.respondError(ctx, .bad_request, "Username Unavailable"),
|
||||
else => return err,
|
||||
};
|
||||
|
||||
try utils.respondJson(ctx, .created, user);
|
||||
}
|
||||
|
||||
pub fn login(srv: *RequestServer, ctx: *http.server.Context, _: RouteArgs) !void {
|
||||
const credentials = try utils.parseRequestBody(struct { username: []const u8, password: []const u8 }, ctx);
|
||||
defer utils.freeRequestBody(credentials, ctx.alloc);
|
||||
|
||||
var api = try utils.getApiConn(srv, ctx);
|
||||
defer api.close();
|
||||
|
||||
const token = api.login(credentials.username, credentials.password) catch |err| switch (err) {
|
||||
error.PasswordVerificationFailed => return utils.respondError(ctx, .bad_request, "Invalid Login"),
|
||||
else => return err,
|
||||
};
|
||||
|
||||
try utils.respondJson(ctx, .ok, token);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -86,7 +86,7 @@ fn getAlloc(row: sql.Row, comptime T: type, idx: u15, alloc: ?std.mem.Allocator)
|
|||
DateTime => row.getDateTime(idx),
|
||||
|
||||
else => switch (@typeInfo(T)) {
|
||||
.Optional => if (row.isNull(idx))
|
||||
.Optional => if (try row.isNull(idx))
|
||||
null
|
||||
else
|
||||
try getAlloc(row, std.meta.Child(T), idx, alloc),
|
||||
|
@ -98,6 +98,8 @@ fn getAlloc(row: sql.Row, comptime T: type, idx: u15, alloc: ?std.mem.Allocator)
|
|||
|
||||
.Enum => try getEnum(row, T, idx),
|
||||
|
||||
.Int => @intCast(T, try row.getI64(idx)),
|
||||
|
||||
//else => unreachable,
|
||||
else => @compileError("unknown type " ++ @typeName(T)),
|
||||
},
|
||||
|
|
|
@ -153,13 +153,15 @@ const migrations: []const Migration = &.{
|
|||
\\ id TEXT NOT NULL PRIMARY KEY,
|
||||
\\
|
||||
\\ name TEXT NOT NULL,
|
||||
\\ invite_code TEXT NOT NULL UNIQUE,
|
||||
\\ code TEXT NOT NULL UNIQUE,
|
||||
\\ created_by TEXT NOT NULL REFERENCES local_user(id),
|
||||
\\
|
||||
\\ max_uses INTEGER,
|
||||
\\
|
||||
\\ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
\\ expires_at DATETIME
|
||||
\\ expires_at DATETIME,
|
||||
\\
|
||||
\\ type TEXT NOT NULL CHECK (type in ('system', 'community_owner', 'user'))
|
||||
\\);
|
||||
\\ALTER TABLE local_user ADD COLUMN invite_id TEXT REFERENCES invite(id);
|
||||
,
|
||||
|
@ -174,6 +176,7 @@ const migrations: []const Migration = &.{
|
|||
\\CREATE TABLE community(
|
||||
\\ id TEXT NOT NULL PRIMARY KEY,
|
||||
\\
|
||||
\\ owner_id TEXT REFERENCES user(id),
|
||||
\\ name TEXT NOT NULL,
|
||||
\\ host TEXT NOT NULL UNIQUE,
|
||||
\\ scheme TEXT NOT NULL CHECK (scheme IN ('http', 'https')),
|
||||
|
|
|
@ -26,6 +26,8 @@ const router = Router{
|
|||
|
||||
prepare(c.invites.create),
|
||||
|
||||
prepare(c.users.create),
|
||||
|
||||
//Route.new(.POST, "/notes", &c.notes.create),
|
||||
//Route.new(.GET, "/notes/:id", &c.notes.get),
|
||||
|
||||
|
|
|
@ -8,6 +8,10 @@ pub fn now() DateTime {
|
|||
return .{ .seconds_since_epoch = std.time.timestamp() };
|
||||
}
|
||||
|
||||
pub fn isAfter(lhs: DateTime, rhs: DateTime) bool {
|
||||
return lhs.seconds_since_epoch > rhs.seconds_since_epoch;
|
||||
}
|
||||
|
||||
pub fn epochSeconds(value: DateTime) std.time.epoch.EpochSeconds {
|
||||
return .{ .secs = @intCast(u64, value.seconds_since_epoch) };
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ pub fn eql(lhs: ?Uuid, rhs: ?Uuid) bool {
|
|||
if (lhs == null and rhs == null) return true;
|
||||
if (lhs == null or rhs == null) return false;
|
||||
|
||||
return lhs.data == rhs.data;
|
||||
return lhs.?.data == rhs.?.data;
|
||||
}
|
||||
|
||||
pub fn toCharArray(value: Uuid) [string_len]u8 {
|
||||
|
|
|
@ -5,6 +5,12 @@ pub const Uuid = @import("./Uuid.zig");
|
|||
pub const DateTime = @import("./DateTime.zig");
|
||||
pub const PathIter = @import("./PathIter.zig");
|
||||
|
||||
pub fn cloneStr(str: []const u8, alloc: std.mem.Allocator) ![]const u8 {
|
||||
var new = try alloc.alloc(u8, str.len);
|
||||
std.mem.copy(u8, new, str);
|
||||
return new;
|
||||
}
|
||||
|
||||
pub const case = struct {
|
||||
// returns the number of capital letters in a string.
|
||||
// only works with ascii characters
|
||||
|
|
Loading…
Reference in New Issue